package com.inetools.gwt.gajaxs.client;

import java.util.Date;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SimpleHtmlSanitizer;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.inetools.gwetons.client.RegExpJsni;
import com.inetools.gwetons.client.RegExpJsniIntf;
import com.inetools.gwt.gajaxs.client.GCalenderFeedData.Entry;
import com.inetools.gwt.gajaxs.client.GDataEventKindFeedData.EntryWhen;
import com.inetools.gwt.gajaxs.client.GDataEventKindFeedData.EntryWhere;
import com.inetools.gwt.gajaxs.client.GDataFeedModel.Link;

public class GCalevenderLiteViewerWidget extends Composite implements GCalevenderLite.GCalevenderLiteListener {
	// protected VerticalPanel verticalPanel = new VerticalPanel();
	interface GCalevenderLiteViewerWidgetUiBinder extends UiBinder<Widget, GCalevenderLiteViewerWidget> {
	}
	protected GCalevenderLiteViewerWidgetUiBinder uiBinder = 
		GWT.create( GCalevenderLiteViewerWidgetUiBinder.class);
	@UiField HTMLPanel containerHTMLPanel;
	@UiField VerticalPanel titleVerticalPanel;
//	@UiField HTML subtitleHTML;
	@UiField VerticalPanel entryVerticalPanel;
	
	protected GCalevenderLite gCalevenderLite = new GCalevenderLite();
	protected GCalevenderLiteViewerWidgetMessages gCalevenderLiteViewerWidgetMessages = 
		GWT.create( GCalevenderLiteViewerWidgetMessages.class);
	
	public GCalevenderLiteViewerWidget( GCalenderDataApiInputOptions gCalenderDataApiInputOptions) {
		gCalevenderLite.setGCalenderDataApiInputOptions( gCalenderDataApiInputOptions);
		gCalevenderLite.addGCalevenderLiteListener( this);
		
		initWidget( uiBinder.createAndBindUi( this));
	}

	public void showEvents() {
		gCalevenderLite.requestEventData();
	}
	
	protected String getAlternateLink( JsArray<Link> linkJsArray) {
		if ( linkJsArray == null) return null;
		
		for( int index = 0;  index < linkJsArray.length(); index++) {
			Link link = linkJsArray.get( index);
			if ( "alternate".equalsIgnoreCase( link.getRel())) { //$NON-NLS-1$
				return link.getHref();
			}
		} // for
		return null;
	} // protected String getAlternateLink( Link link)
	
	protected RegExpJsniIntf rfc3339DateRegExp 
	= new RegExpJsni( "^\\d{4}\\-\\d{2}\\-\\d{2}", true, false, false); //$NON-NLS-1$
	protected JavaScriptObject rfc3339TimeRegExp = 
		RegExpJsni.getRegExp( "T\\d{2}\\:\\d{2}\\:\\d{2}\\.\\d{3}(Z|(\\+|\\-)\\d{2}\\:\\d{2})$", true, false); //$NON-NLS-1$
	
	protected DateTimeFormat dateParseFormat = DateTimeFormat.getFormat( "yyyy-MM-dd"); //$NON-NLS-1$
	protected DateTimeFormat dateTimeParseFormat = DateTimeFormat.getFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); //$NON-NLS-1$
	protected DateTimeFormat localDateTimeParseFormat = DateTimeFormat.getFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); //$NON-NLS-1$
	
	protected DateTimeFormat dateFormat = DateTimeFormat.getFormat( "EEE, MMM d, ''yy"); //$NON-NLS-1$
	/**
	 * @return DateTimeFormat class object which will be used as format to show schedule with date information such like all day event.
	 * Default value of dateFormat is "EEE, MMM d, ''yy".
	 */
	public DateTimeFormat getDateFormat() {
		return dateFormat;
	}
	/**
	 * @param dateFormat: format which will be used to show schedule with date information such like all day event.
	 * Default value of dateFormat is "EEE, MMM d, ''yy".
	 */
	public void setDateFormat(DateTimeFormat dateFormat) {
		this.dateFormat = dateFormat;
	}

	protected DateTimeFormat dateTimeFormat = DateTimeFormat.getFormat( "EEE, MMM d, ''yy 'at' HH:mm zzzz"); //$NON-NLS-1$
	/**
	 * @return DateTimeFormat class object which will be used as format to show schedule with date and time information.
	 * Default value of dataTimeFormat is "EEE, MMM d, ''yy 'at' HH:mm zzzz".
	 */
	public DateTimeFormat getDateTimeFormat() {
		return dateTimeFormat;
	}
	/**
	 * @param dateTimeFormat: format which will be used as format to show schedule with date and time information.
	 * Default value of dataTimeFormat is "EEE, MMM d, ''yy 'at' HH:mm zzzz".
	 */
	public void setDateTimeFormat(DateTimeFormat dateTimeFormat) {
		this.dateTimeFormat = dateTimeFormat;
	}
	
	/**
	 * @param rfc3339DateTimeStr: Date (and time) string formatted in RFC3339
	 * @return Date classs object representing rfc3339DateTimeStr value
	 * @throws IllegalArgumentException
	 */
	protected Date getDateFromRfc3339DateTimeStr( String rfc3339DateTimeStr) throws IllegalArgumentException {
		if ( !rfc3339DateRegExp.test( rfc3339DateTimeStr)) {
			throw new IllegalArgumentException( rfc3339DateTimeStr + "is not in RFC 3339 format"); //$NON-NLS-1$
		}
		
		String rightContextStr = rfc3339DateRegExp.getRightContext();
		if ( !RegExpJsni.test( rfc3339TimeRegExp, rightContextStr)) { // startTime contains only date info
				return dateParseFormat.parse( rfc3339DateTimeStr);
		}
		else { // startTime contains date and time info
			if ( rightContextStr.endsWith( "Z")) { // startTime is in local time format //$NON-NLS-1$
				return localDateTimeParseFormat.parse( rfc3339DateTimeStr);
			}
			else {
				return dateTimeParseFormat.parse( rfc3339DateTimeStr);
			}
		}
	} // protected Date getDateFromRfc3339DateTimeStr( String rfc3339DateTimeStr)
	
	/**
	 * @param scheduleStr: RFC 3339 formatted date time string
	 * @return date time string formatted by either dateFormat or dateTimeFormat DateTimeFormat class objects
	 * @throws IllegalArgumentException
	 */
	protected String getDateTimeStr( String scheduleStr) throws IllegalArgumentException {
		try {
			Date scheduleDate = getDateFromRfc3339DateTimeStr( scheduleStr);
			
			rfc3339DateRegExp.test( scheduleStr);
			String rightContextStr = rfc3339DateRegExp.getRightContext();
			if ( !RegExpJsni.test( rfc3339TimeRegExp, rightContextStr)) { // startTime contains only date info
				return dateFormat.format( scheduleDate);
			}
			else {
				return dateTimeFormat.format( scheduleDate);
			}
		}
		catch( IllegalArgumentException illegalArgumentException) {
			throw illegalArgumentException;
		}
	} // protected String getDateTimeStr( String scheduleStr)
	
	protected String additionalStyleNameSuffix = this.getClass().getName();
	{
		if ( additionalStyleNameSuffix.indexOf( ".") > -1) { //$NON-NLS-1$
			int classNameIndex = additionalStyleNameSuffix.lastIndexOf( ".") + 1; //$NON-NLS-1$
			if ( classNameIndex < additionalStyleNameSuffix.length()) {
				additionalStyleNameSuffix = 
					"_" + additionalStyleNameSuffix.substring( classNameIndex); //$NON-NLS-1$
			}
			else {
				additionalStyleNameSuffix = 
					"_" + additionalStyleNameSuffix.replace( '.', '_'); //$NON-NLS-1$
			}
		}
	};
	public final String TITLE_ADDITIONAL_STYLE_NAME = "title" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String SUBTITLE__ADDITIONAL_STYLE_NAME = "subTitle" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String NO_SCHEDULE_DATA_ADDITIONAL_STYLE_NAME = "noScheduleData" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String EVENT_ENTRY_ADDITIONAL_STYLE_NAME = "eventEntry" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String EVENT_ENTRY_TITLE_ADDITIONAL_STYLE_NAME = "eventTitle" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String CONTENT_ADDITIONAL_STYLE_NAME = "eventContent" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String DATE_INFO_PANEL_ADDITIONAL_STYLE_NAME = 
		"eventDateInfoPaneBy" + this.getClass().getName(); //$NON-NLS-1$
	public final String DATE_INFO_ADDITIONAL_STYLE_NAME = "eventDateInfo" + additionalStyleNameSuffix; //$NON-NLS-1$
	public final String PLACE_INFO_PANEL_ADDITIONAL_STYLE_NAME = 
		"eventPlaceInfoPaneBy" + this.getClass().getName(); //$NON-NLS-1$
	public final String PLACE_INFO_ADDITIONAL_STYLE_NAME = "eventPlaceInfo" + additionalStyleNameSuffix; //$NON-NLS-1$
	
	protected int maxEventsBeingExpanded = 5;
	/**
	 * @return maximum number (default is 5) of events expanded to show the event's detail info
	 */
	public int getMaxEventsBeingExpanded() {
		return maxEventsBeingExpanded;
	}
	/**
	 * Set maximum number (default is 5) of events expanded to show the event's detail info
	 * @param maxExpandedEvents
	 */
	public void setMaxEventsBeingExpanded(int maxEventsBeingExpanded) {
		if ( maxEventsBeingExpanded > -1) this.maxEventsBeingExpanded = maxEventsBeingExpanded;
	}

	protected int daysBeingExpanded = 31;
	public int getDaysBeingExpanded() {
		return daysBeingExpanded;
	}
	public void setDaysBeingExpanded( int numerOfdays) {
		if ( numerOfdays > 0) daysBeingExpanded = numerOfdays;
	}
	
	protected String noScheduleDataErrorMsg = gCalevenderLiteViewerWidgetMessages.noScheduleData();
	/**
	 * @return Message string what will be shown on the case of no available schedule data
	 */
	public String getNoScheduleDataErrorMsg() {
		return noScheduleDataErrorMsg;
	}
	/**
	 * @param noScheduleDataErrorMsg: Message string what will be shown on the case of no available schedule data
	 */
	public void setNoScheduleDataErrorMsg(String noScheduleDataErrorMsg) {
		this.noScheduleDataErrorMsg = noScheduleDataErrorMsg;
	}

	/**
	 * This is listener method for events on GCalevenderLite class object.
	 * This shows event info: title, subtitle (description), when (start and end times), where.
	 * If event happens within 1 month, then event node will be shown expanded to show when and where info. 
	 */
	@Override
	public void onCallback( GCalenderFeedData gCalenderFeedData) {
		
		// About title 
		String titleUrl = getAlternateLink( gCalenderFeedData.getLinks());
		if ( titleUrl != null) {
			Anchor titleAnchor = new Anchor();
			titleAnchor.setText( gCalenderFeedData.getTitle());
			titleAnchor.setWordWrap( true);
			titleAnchor.setTarget( "_blank"); //$NON-NLS-1$
			titleAnchor.setHref( titleUrl);
			titleAnchor.addStyleName( TITLE_ADDITIONAL_STYLE_NAME);
			titleVerticalPanel.add( titleAnchor);
		}
		else {
			Label titleLabel = new Label( gCalenderFeedData.getTitle());
			titleLabel.setWordWrap( true);
			titleLabel.addStyleName( TITLE_ADDITIONAL_STYLE_NAME);
			titleVerticalPanel.add( titleLabel);
		}
		
		
		// About subtitle
		SafeHtml subTitleSafeHtml = SimpleHtmlSanitizer.sanitizeHtml( gCalenderFeedData.getSubTitle());
		HTML subtitleHTML = new HTML( subTitleSafeHtml);
		subtitleHTML.setWordWrap( true);
		subtitleHTML.addStyleName( SUBTITLE__ADDITIONAL_STYLE_NAME);
		titleVerticalPanel.add( subtitleHTML);
		
		// About entries
		JsArray<Entry> entryJsArray = gCalenderFeedData.getEntries();
		
		if ( entryJsArray.length() < 1) {
			Label noFutureScheduleErrorMsgLabel = new Label( noScheduleDataErrorMsg, true); 
			noFutureScheduleErrorMsgLabel.addStyleName( NO_SCHEDULE_DATA_ADDITIONAL_STYLE_NAME);
			entryVerticalPanel.add( noFutureScheduleErrorMsgLabel);
			return;
		}
		
		
		long singleDayMillisec = 86400000L;
		long detailInfoExpantionRange = System.currentTimeMillis() + daysBeingExpanded * singleDayMillisec;
		int expandedEventCount = 0;
		
		for( int index = 0;  index < entryJsArray.length(); index++) {
			Tree tree = new Tree();
			tree.setAnimationEnabled( true);
			
			Entry entry = entryJsArray.get( index);
			
			// About title ------------------------------------------------------------------------
			String entryTitleStr = entry.getTitle();
			tree.setTitle( entryTitleStr);
			
			TreeItem treeItem; // Caution: Default white-space css style setting for child item of treeItem is nowrap
			
			String entryTitleUrl = getAlternateLink( entry.getLinks());
			if ( ( entryTitleUrl == null) || ( "".equals( entryTitleUrl))) { //$NON-NLS-1$
				Label entryTitleLabel = new Label( entryTitleStr);
				entryTitleLabel.setWordWrap( true);
				entryTitleLabel.addStyleName( EVENT_ENTRY_TITLE_ADDITIONAL_STYLE_NAME);
				treeItem = tree.addItem( entryTitleLabel);
			}
			else {
				Anchor entryTitleAnchor = new Anchor( entryTitleStr);
				entryTitleAnchor.setHref( entryTitleUrl);
				entryTitleAnchor.setWordWrap( true);
				entryTitleAnchor.addStyleName( EVENT_ENTRY_TITLE_ADDITIONAL_STYLE_NAME);
				treeItem = tree.addItem( entryTitleAnchor);
			}
			// ------------------------------------------------------------------------------------
			
//			treeItem.setState( true);	// Make it appear opened
			
			// About content ----------------------------------------------------------------------
			String contentStr = entry.getContent();
			if ( contentStr != null) {
				if ( !"".equals( contentStr)) { //$NON-NLS-1$
					// TODO If content cannot contain HTML element such as a tag, then not use HTML widget and just add contentStr to treeItem
					HTML contentHTML 
					= new HTML( SimpleHtmlSanitizer.sanitizeHtml( contentStr));
					contentHTML.setWordWrap( true);
					contentHTML.addStyleName( CONTENT_ADDITIONAL_STYLE_NAME);
					treeItem.addItem( contentHTML);
				}
			}
			// ------------------------------------------------------------------------------------
		
			// About when -------------------------------------------------------------------------
			// Refer to http://code.google.com/apis/gdata/docs/2.0/elements.html#gdWhen
			JsArray<EntryWhen> entryWhenJsArray = entry.getWhenArray();
			boolean eventInfoExpandOn = false; // When this is true, treeItem will be appear as expanded
			if ( entryWhenJsArray != null) {
				
				for( int subIndex = 0; subIndex < entryWhenJsArray.length(); subIndex++) {
					EntryWhen entryWhen = entryWhenJsArray.get( subIndex);
					
					DisclosurePanel disclosurePanel = 
						new DisclosurePanel( gCalevenderLiteViewerWidgetMessages.dateTimeTable()); 
					disclosurePanel.setAnimationEnabled( true);
					disclosurePanel.setOpen( true);
					
					// About startTime
					//String whenStr = "Schedule: <br />";
					String whenStr = ""; //$NON-NLS-1$
					
					try {
						String startTimeStr = getDateTimeStr( entryWhen.getStartTime());
						whenStr = 
							whenStr + gCalevenderLiteViewerWidgetMessages.startAt() + startTimeStr; 
						long scheduleDateLong = 
							getDateFromRfc3339DateTimeStr( entryWhen.getStartTime()).getTime();
						if ( scheduleDateLong <= detailInfoExpantionRange) { // Event will happen within 31 days from now
							eventInfoExpandOn = true;
						}
					}
					catch( Exception exception) {
GWT.log( "startTime value (\"" + entryWhen.getStartTime() + "\") for " + entryTitleStr + " event is not in RFC 3339 format", null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						continue; // for
					}
					
					// About endTime
					// endTime could be null. If endTime is not specified, then the event is 
					// considered to be an instant in time (if @startTime includes a time) or 
					// a one-day event (if @startTime is only a date).
					String endTimeStr = entryWhen.getEndTime();
					if ( endTimeStr == null) {
						whenStr = 
							whenStr + gCalevenderLiteViewerWidgetMessages.allDay();
					}
					else {
						try {
							endTimeStr = getDateTimeStr( endTimeStr);
							whenStr = 
								whenStr + gCalevenderLiteViewerWidgetMessages.endAt() + endTimeStr; 
						}
						catch( Exception exception) {
GWT.log( "endTime value (\"" + entryWhen.getEndTime() + "\") for " + entryTitleStr + " event is not in RFC 3339 format", null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						}
					}
					
					HTML whenHTML = new HTML( SimpleHtmlSanitizer.sanitizeHtml( whenStr));
					whenHTML.setWordWrap( true);
					whenHTML.addStyleName( DATE_INFO_ADDITIONAL_STYLE_NAME);
					//treeItem.addItem( whenHTML);
					disclosurePanel.add( whenHTML);
					disclosurePanel.addStyleName( DATE_INFO_PANEL_ADDITIONAL_STYLE_NAME);
					treeItem.addItem( disclosurePanel);
				} // for
			}
			// ------------------------------------------------------------------------------------
			
			// About where ------------------------------------------------------------------------
			// http://code.google.com/apis/gdata/docs/2.0/elements.html#gdWhere
			JsArray<EntryWhere> entryWhereJsArray = entry.getWhereArray();
			if ( entryWhereJsArray != null) {
				for( int subIndex = 0; subIndex < entryWhereJsArray.length(); subIndex++) {
					EntryWhere entryWhere = entryWhereJsArray.get( subIndex);
					String whereStr = entryWhere.getValueString();
					if ( whereStr != null) {
						if ( !"".equals( whereStr)) { //$NON-NLS-1$
							DisclosurePanel disclosurePanel = 
								new DisclosurePanel( gCalevenderLiteViewerWidgetMessages.place()); 
							disclosurePanel.setAnimationEnabled( true);
							disclosurePanel.setOpen( true);
							
							// HTML whereHTML = new HTML( "Place: " + whereStr);
							// treeItem.addItem( whereHTML);
							
							HTML whereHTML = new HTML( SimpleHtmlSanitizer.sanitizeHtml( whereStr));
							whereHTML.setWordWrap( true);
							whereHTML.addStyleName( PLACE_INFO_ADDITIONAL_STYLE_NAME);
							disclosurePanel.add( whereHTML);
							disclosurePanel.addStyleName( PLACE_INFO_PANEL_ADDITIONAL_STYLE_NAME);
							treeItem.addItem( disclosurePanel);
						}
					}
				} // for
			}
			// ------------------------------------------------------------------------------------
			
			treeItem.addStyleName( EVENT_ENTRY_ADDITIONAL_STYLE_NAME);
			
			if ( expandedEventCount < getMaxEventsBeingExpanded()) {
				if ( eventInfoExpandOn) {
					// Make treeItem appear opened. 
					// For setState method to work, one child item at least needs to have been added before.
					treeItem.setState( true);
					expandedEventCount++;
				}
			}
			
			entryVerticalPanel.add( tree);
		} // for
	} // public void onCallback( GCalenderFeedData gCalenderFeedData)

	/**
	 * The message displayed to the user when the server cannot be reached or
	 * returns an error.
	 */
	protected String serverErrorMsg = gCalevenderLiteViewerWidgetMessages.connectionError();
	/**
	 * @return The message displayed to the user when the server cannot be reached or returns an error.
	 * Default message is "An error occurred while attempting to contact the server."
	 */
	public String getServerErrorMsg() {
		return serverErrorMsg;
	}
	/**
	 * @param serverError: the message displayed to the user when the server cannot be reached or returns an error.
	 * Default message is "An error occurred while attempting to contact the server."
	 */
	public void setServerErrorMsg(String serverErrorMsg) {
		this.serverErrorMsg = serverErrorMsg;
	}
	public final String SERVER_ERROR_MSG_ADDITIONAL_STYLE_NAME = "serverErrorMessage" + additionalStyleNameSuffix; //$NON-NLS-1$

	/**
	 * This is listener method for events on GCalevenderLite class object.
	 */
	@Override
	public void onFailure() {
		titleVerticalPanel.clear();
		entryVerticalPanel.clear();
		
		Label serverErrorLabel = new Label( getServerErrorMsg());
		serverErrorLabel.addStyleName( SERVER_ERROR_MSG_ADDITIONAL_STYLE_NAME);
		serverErrorLabel.setWordWrap( true);
		titleVerticalPanel.add( serverErrorLabel);
	}

}
